Skip to content

Commit 6754855

Browse files
feat: round robin host selection strategy (#603)
1 parent 61b2698 commit 6754855

18 files changed

+728
-59
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`. <br><br> **Note:** The `<weight>` value in the string must be an integer greater than or equal to 1. | `null` |
98+
| | `roundRobinDefaultWeight` | This parameter value must be an integer 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. <br><br> **Note:** This value must be an integer greater than or equal to 1. | `1` |
99+
87100
### Limitations
88101

89102
#### General plugin limitations

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Properties;
2323
import org.checkerframework.checker.nullness.qual.NonNull;
24+
import org.checkerframework.checker.nullness.qual.Nullable;
2425
import software.amazon.jdbc.dialect.Dialect;
2526

2627
/**
@@ -59,12 +60,13 @@ boolean acceptsUrl(
5960
* @param role determines if the connection provider should return a writer or a reader
6061
* @param strategy the strategy determining how the {@link HostSpec} should be selected, e.g.,
6162
* random or round-robin
63+
* @param props any properties that are required by the provided strategy to select a host
6264
* @return the {@link HostSpec} selected using the specified strategy
6365
* @throws SQLException if an error occurred while returning the hosts
6466
* @throws UnsupportedOperationException if the strategy is unsupported by the provider
6567
*/
6668
HostSpec getHostSpecByStrategy(
67-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
69+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @Nullable Properties props)
6870
throws SQLException, UnsupportedOperationException;
6971

7072
/**

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,22 @@ public boolean acceptsStrategy(HostRole role, String strategy) {
134134
* @param role the desired role of the host - either a writer or a reader
135135
* @param strategy the strategy that should be used to select a {@link HostSpec} from the host
136136
* list (eg "random")
137+
* @param props any properties that are required by the provided strategy to select a host
137138
* @return a {@link HostSpec} with the requested role
138139
* @throws SQLException if the available {@link ConnectionProvider} instances
139140
* cannot find a host in the host list matching the
140141
* requested role or an error occurs while selecting a host
141142
* @throws UnsupportedOperationException if the available {@link ConnectionProvider} instances do
142143
* not support the requested strategy
143144
*/
144-
public HostSpec getHostSpecByStrategy(List<HostSpec> hosts, HostRole role, String strategy)
145+
public HostSpec getHostSpecByStrategy(List<HostSpec> hosts, HostRole role, String strategy, Properties props)
145146
throws SQLException, UnsupportedOperationException {
146147
HostSpec host = null;
147148
if (connProvider != null) {
148149
connProviderLock.readLock().lock();
149150
try {
150151
if (connProvider != null && connProvider.acceptsStrategy(role, strategy)) {
151-
host = connProvider.getHostSpecByStrategy(hosts, role, strategy);
152+
host = connProvider.getHostSpecByStrategy(hosts, role, strategy, props);
152153
}
153154
} catch (UnsupportedOperationException e) {
154155
// The custom provider does not support the provided strategy, ignore it and try with the default provider.
@@ -158,7 +159,7 @@ public HostSpec getHostSpecByStrategy(List<HostSpec> hosts, HostRole role, Strin
158159
}
159160

160161
if (host == null) {
161-
host = defaultProvider.getHostSpecByStrategy(hosts, role, strategy);
162+
host = defaultProvider.getHostSpecByStrategy(hosts, role, strategy, props);
162163
}
163164

164165
return host;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.logging.Logger;
2828
import javax.sql.DataSource;
2929
import org.checkerframework.checker.nullness.qual.NonNull;
30+
import org.checkerframework.checker.nullness.qual.Nullable;
3031
import software.amazon.jdbc.dialect.Dialect;
3132
import software.amazon.jdbc.exceptions.SQLLoginException;
3233
import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect;
@@ -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;
@@ -86,7 +88,7 @@ public boolean acceptsStrategy(@NonNull HostRole role, @NonNull String strategy)
8688

8789
@Override
8890
public HostSpec getHostSpecByStrategy(
89-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
91+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @Nullable Properties props)
9092
throws SQLException {
9193
if (!acceptedStrategies.containsKey(strategy)) {
9294
throw new UnsupportedOperationException(
@@ -95,7 +97,7 @@ public HostSpec getHostSpecByStrategy(
9597
new Object[] {strategy, DataSourceConnectionProvider.class}));
9698
}
9799

98-
return acceptedStrategies.get(strategy).getHost(hosts, role);
100+
return acceptedStrategies.get(strategy).getHost(hosts, role, props);
99101
}
100102

101103
/**

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Properties;
2626
import java.util.logging.Logger;
2727
import org.checkerframework.checker.nullness.qual.NonNull;
28+
import org.checkerframework.checker.nullness.qual.Nullable;
2829
import software.amazon.jdbc.dialect.Dialect;
2930
import software.amazon.jdbc.exceptions.SQLLoginException;
3031
import software.amazon.jdbc.targetdriverdialect.ConnectInfo;
@@ -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

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

8284
@Override
8385
public HostSpec getHostSpecByStrategy(
84-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
86+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy, @Nullable Properties props)
8587
throws SQLException {
8688
if (!acceptedStrategies.containsKey(strategy)) {
8789
throw new UnsupportedOperationException(
@@ -90,7 +92,7 @@ public HostSpec getHostSpecByStrategy(
9092
new Object[] {strategy, DriverConnectionProvider.class}));
9193
}
9294

93-
return acceptedStrategies.get(strategy).getHost(hosts, role);
95+
return acceptedStrategies.get(strategy).getHost(hosts, role, props);
9496
}
9597

9698
/**

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

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@
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;
2930
import java.util.concurrent.TimeUnit;
3031
import java.util.logging.Logger;
3132
import java.util.stream.Collectors;
3233
import org.checkerframework.checker.nullness.qual.NonNull;
34+
import org.checkerframework.checker.nullness.qual.Nullable;
3335
import software.amazon.jdbc.cleanup.CanReleaseResources;
3436
import software.amazon.jdbc.dialect.Dialect;
3537
import software.amazon.jdbc.util.HikariCPSQLException;
@@ -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(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
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<>(
@@ -56,6 +62,7 @@ public class HikariPooledConnectionProvider implements PooledConnectionProvider,
5662
private static long poolExpirationCheckNanos = TimeUnit.MINUTES.toNanos(30);
5763
private final HikariPoolConfigurator poolConfigurator;
5864
private final HikariPoolMapping poolMapping;
65+
private final LeastConnectionsHostSelector leastConnectionsHostSelector;
5966

6067
/**
6168
* {@link HikariPooledConnectionProvider} constructor. This class can be passed to
@@ -98,6 +105,7 @@ public HikariPooledConnectionProvider(
98105
HikariPoolConfigurator hikariPoolConfigurator, HikariPoolMapping mapping) {
99106
this.poolConfigurator = hikariPoolConfigurator;
100107
this.poolMapping = mapping;
108+
this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools);
101109
}
102110

103111
/**
@@ -134,6 +142,7 @@ public HikariPooledConnectionProvider(
134142
this.poolMapping = mapping;
135143
poolExpirationCheckNanos = poolExpirationNanos;
136144
databasePools.setCleanupIntervalNanos(poolCleanupNanos);
145+
this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools);
137146
}
138147

139148
@Override
@@ -145,44 +154,28 @@ public boolean acceptsUrl(
145154

146155
@Override
147156
public boolean acceptsStrategy(@NonNull HostRole role, @NonNull String strategy) {
148-
return LEAST_CONNECTIONS_STRATEGY.equals(strategy);
157+
return acceptedStrategies.containsKey(strategy)
158+
|| LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS.equals(strategy);
149159
}
150160

151161
@Override
152162
public HostSpec getHostSpecByStrategy(
153-
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @NonNull String strategy)
154-
throws SQLException {
155-
if (!LEAST_CONNECTIONS_STRATEGY.equals(strategy)) {
163+
@NonNull List<HostSpec> hosts,
164+
@NonNull HostRole role,
165+
@NonNull String strategy,
166+
@Nullable Properties props) throws SQLException {
167+
if (!acceptsStrategy(role, strategy)) {
156168
throw new UnsupportedOperationException(
157169
Messages.get(
158170
"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}));
171+
new Object[] {strategy, DataSourceConnectionProvider.class}));
171172
}
172173

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();
174+
if (LeastConnectionsHostSelector.STRATEGY_LEAST_CONNECTIONS.equals(strategy)) {
175+
return this.leastConnectionsHostSelector.getHost(hosts, role, props);
176+
} else {
177+
return acceptedStrategies.get(strategy).getHost(hosts, role, props);
184178
}
185-
return numConnections;
186179
}
187180

188181
@Override
@@ -338,6 +331,10 @@ public PoolKey(final @NonNull String url, final @NonNull String extraKey) {
338331
this.extraKey = extraKey;
339332
}
340333

334+
public String getUrl() {
335+
return this.url;
336+
}
337+
341338
@Override
342339
public int hashCode() {
343340
final int prime = 31;

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

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

1919
import java.sql.SQLException;
2020
import java.util.List;
21+
import java.util.Properties;
22+
import org.checkerframework.checker.nullness.qual.NonNull;
23+
import org.checkerframework.checker.nullness.qual.Nullable;
2124

2225
public interface HostSelector {
2326

2427
/**
2528
* Selects a host with the requested role from the given host list.
2629
*
27-
* @param hosts a list of available hosts to pick from
28-
* @param role the desired host role - either a writer or a reader
30+
* @param hosts a list of available hosts to pick from.
31+
* @param role the desired host role - either a writer or a reader.
32+
* @param props connection properties that may be needed by the host selector in order to choose a host.
2933
* @return a host matching the requested role
3034
* @throws SQLException if the host list does not contain any hosts matching the requested role or
3135
* an error occurs while selecting a host
3236
*/
33-
HostSpec getHost(List<HostSpec> hosts, HostRole role) throws SQLException;
37+
HostSpec getHost(
38+
@NonNull List<HostSpec> hosts, @NonNull HostRole role, @Nullable Properties props) throws SQLException;
3439
}

0 commit comments

Comments
 (0)