Skip to content

Commit d43b211

Browse files
authored
Merge branch 'main' into fix/isValid-zero
2 parents 10a6249 + 45b9366 commit d43b211

16 files changed

+502
-56
lines changed

README.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,10 @@ To find all the documentation and concrete examples on how to use the AWS JDBC D
134134

135135
#### Amazon RDS Blue/Green Deployments
136136

137-
**Important: Service Dependency**
138-
139-
Support for Blue/Green deployments using the AWS Advanced JDBC Wrapper requires specific metadata tables that are **not available in the current RDS and Aurora service**. Please contact your AWS account team for metadata release timelines.
140-
141-
**Limitations:**
142-
143137
- **Post-switchover failures:** After a Blue/Green switchover, the wrapper may not properly detect the new cluster topology, leading to failed failover attempts.
144138
- **Metadata inconsistencies:** Discrepancies between topology metadata and actual available endpoints prevent reliable operation.
145139
- **Version-specific issues:** Requirements vary between Aurora MySQL and Aurora PostgreSQL due to different internal systems.
146140

147-
**If You Must Use Blue/Green (Not Recommended for Production):**
148-
149-
1. Enable the `enableGreenNodeReplacement` configuration parameter.
150-
2. Thoroughly test in non-production environments.
151-
152141
**Recommendation:**
153142

154143
We advise waiting for the RDS service update before enabling the Blue/Green Deployments plugin. If the metadata table does not exist, your application will continue to work; however, errors will be logged stating that relevant Blue/Green metadata cannot be found.

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ The [Blue/Green Deployment](https://docs.aws.amazon.com/whitepapers/latest/blue-
66

77
The AWS JDBC Driver leverages the Blue/Green Deployment approach by intelligently managing traffic distribution between blue and green nodes, minimizing the impact of stale DNS data and connectivity disruptions on user applications.
88

9-
**Important: Service Dependency**
10-
11-
Support for Blue/Green deployments using the AWS Advanced JDBC Wrapper requires specific metadata tables that are **not available in the current RDS and Aurora service**. Please contact your AWS account team for metadata release timelines.
12-
139
## Prerequisites
1410
> [!WARNING]\
1511
> Currently Supported Database Deployments:
@@ -26,7 +22,13 @@ Support for Blue/Green deployments using the AWS Advanced JDBC Wrapper requires
2622
>
2723
> **Blue/Green Support Behaviour and Version Compatibility:**
2824
>
29-
> The AWS Advanced JDBC Wrapper now includes enhanced full support for Blue/Green Deployments. This support requires a minimum database version that includes a specific metadata table. This constraint **does not** apply to RDS MySQL.
25+
> The AWS Advanced JDBC Wrapper now includes enhanced full support for Blue/Green Deployments. This support requires a minimum database version that includes a specific metadata table. The metadata will be accessible provided the green deployment satisfies the minimum version compatibility requirements. This constraint **does not** apply to RDS MySQL.
26+
>
27+
> For RDS Postgres, you will also need to manually install the `rds_tools` extension using the following DDL so that the metadata required by the wrapper is available:
28+
>
29+
> ```sql
30+
> CREATE EXTENSION rds_tools;
31+
> ```
3032
>
3133
> If your database version does **not** support this table, the driver will automatically detect its absence and fallback to its previous behaviour. In this fallback mode, Blue/Green handling is subject to the same limitations listed above.
3234
>

wrapper/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ dependencies {
5555
compileOnly("org.postgresql:postgresql:42.7.7")
5656
compileOnly("org.mariadb.jdbc:mariadb-java-client:3.5.6")
5757
compileOnly("org.osgi:org.osgi.core:6.0.0")
58-
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.1.21")
58+
compileOnly("org.jetbrains.kotlin:kotlin-stdlib:2.2.20")
5959

6060
// The following dependency will be included in federated-auth bundle jar.
6161
federatedAuthBundleImplementation("org.apache.httpcomponents:httpclient:4.5.14")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class C3P0PooledConnectionProvider implements PooledConnectionProvider, C
5050
put(HighestWeightHostSelector.STRATEGY_HIGHEST_WEIGHT, new HighestWeightHostSelector());
5151
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
5252
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
53+
put(WeightedRandomHostSelector.STRATEGY_WEIGHTED_RANDOM, new WeightedRandomHostSelector());
5354
}
5455
});
5556
protected static final long poolExpirationCheckNanos = TimeUnit.MINUTES.toNanos(30);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public class DataSourceConnectionProvider implements ConnectionProvider {
5353
put(HighestWeightHostSelector.STRATEGY_HIGHEST_WEIGHT, new HighestWeightHostSelector());
5454
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
5555
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
56+
put(WeightedRandomHostSelector.STRATEGY_WEIGHTED_RANDOM, new WeightedRandomHostSelector());
5657
}
5758
});
5859
private final @NonNull DataSource dataSource;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class DriverConnectionProvider implements ConnectionProvider {
5050
put(HighestWeightHostSelector.STRATEGY_HIGHEST_WEIGHT, new HighestWeightHostSelector());
5151
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
5252
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
53+
put(WeightedRandomHostSelector.STRATEGY_WEIGHTED_RANDOM, new WeightedRandomHostSelector());
5354
}
5455
});
5556

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public class HikariPooledConnectionProvider implements PooledConnectionProvider,
5757
put(HighestWeightHostSelector.STRATEGY_HIGHEST_WEIGHT, new HighestWeightHostSelector());
5858
put(RandomHostSelector.STRATEGY_RANDOM, new RandomHostSelector());
5959
put(RoundRobinHostSelector.STRATEGY_ROUND_ROBIN, new RoundRobinHostSelector());
60+
put(WeightedRandomHostSelector.STRATEGY_WEIGHTED_RANDOM, new WeightedRandomHostSelector());
6061
}
6162
});
6263

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 java.sql.SQLException;
20+
import java.util.Comparator;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Properties;
25+
import java.util.Random;
26+
import java.util.concurrent.locks.ReentrantLock;
27+
import java.util.regex.Matcher;
28+
import java.util.regex.Pattern;
29+
import java.util.stream.Collectors;
30+
import org.checkerframework.checker.nullness.qual.NonNull;
31+
import org.checkerframework.checker.nullness.qual.Nullable;
32+
import software.amazon.jdbc.hostavailability.HostAvailability;
33+
import software.amazon.jdbc.util.Messages;
34+
35+
public class WeightedRandomHostSelector implements HostSelector {
36+
public static final AwsWrapperProperty WEIGHTED_RANDOM_HOST_WEIGHT_PAIRS = new AwsWrapperProperty(
37+
"weightedRandomHostWeightPairs", null,
38+
"Comma separated list of database host-weight pairs in the format of `<host>:<weight>`.");
39+
public static final String STRATEGY_WEIGHTED_RANDOM = "weightedRandom";
40+
static final int DEFAULT_WEIGHT = 1;
41+
static final Pattern HOST_WEIGHT_PAIRS_PATTERN =
42+
Pattern.compile("((?<host>[^:/?#]*):(?<weight>[0-9]*))");
43+
44+
private Map<String, Integer> cachedHostWeightMap;
45+
private String cachedHostWeightMapString;
46+
private Random random;
47+
48+
private final ReentrantLock lock = new ReentrantLock();
49+
50+
public WeightedRandomHostSelector() {
51+
this(new Random());
52+
}
53+
54+
public WeightedRandomHostSelector(final Random random) {
55+
this.random = random;
56+
}
57+
58+
public HostSpec getHost(
59+
@NonNull List<HostSpec> hosts,
60+
@NonNull HostRole role,
61+
@Nullable Properties props) throws SQLException {
62+
63+
final Map<String, Integer> hostWeightMap =
64+
this.getHostWeightPairMap(WEIGHTED_RANDOM_HOST_WEIGHT_PAIRS.getString(props));
65+
66+
// Get and check eligible hosts
67+
final List<HostSpec> eligibleHosts = hosts.stream()
68+
.filter(hostSpec ->
69+
role.equals(hostSpec.getRole()) && hostSpec.getAvailability().equals(HostAvailability.AVAILABLE))
70+
.sorted(Comparator.comparing(HostSpec::getHost))
71+
.collect(Collectors.toList());
72+
73+
if (eligibleHosts.isEmpty()) {
74+
throw new SQLException(Messages.get("HostSelector.noHostsMatchingRole", new Object[] {role}));
75+
}
76+
77+
final Map<String, NumberRange> hostWeightRangeMap = new HashMap<>();
78+
int counter = 1;
79+
for (HostSpec host : eligibleHosts) {
80+
if (!hostWeightMap.containsKey(host.getHost())) {
81+
continue;
82+
}
83+
final int hostWeight = hostWeightMap.get(host.getHost());
84+
if (hostWeight > 0) {
85+
final int rangeStart = counter;
86+
final int rangeEnd = counter + hostWeight - 1;
87+
hostWeightRangeMap.put(host.getHost(), new NumberRange(rangeStart, rangeEnd));
88+
counter = counter + hostWeight;
89+
} else {
90+
hostWeightRangeMap.put(host.getHost(), new NumberRange(counter, counter));
91+
counter++;
92+
}
93+
}
94+
95+
if (this.random == null) {
96+
this.random = new Random();
97+
}
98+
int randomInt = this.random.nextInt(counter);
99+
100+
// Check random number is in host weight range map
101+
for (final HostSpec host : eligibleHosts) {
102+
NumberRange range = hostWeightRangeMap.get(host.getHost());
103+
if (range != null && range.isInRange(randomInt)) {
104+
return host;
105+
}
106+
}
107+
108+
throw new SQLException(Messages.get("HostSelector.weightedRandomUnableToGetHost", new Object[] {role}));
109+
}
110+
111+
private Map<String, Integer> getHostWeightPairMap(final String hostWeightMapString) throws SQLException {
112+
try {
113+
lock.lock();
114+
if (this.cachedHostWeightMapString != null
115+
&& this.cachedHostWeightMapString.trim().equals(hostWeightMapString.trim())
116+
&& this.cachedHostWeightMap != null
117+
&& !this.cachedHostWeightMap.isEmpty()) {
118+
return this.cachedHostWeightMap;
119+
}
120+
121+
final Map<String, Integer> hostWeightMap = new HashMap<>();
122+
if (hostWeightMapString == null || hostWeightMapString.trim().isEmpty()) {
123+
return hostWeightMap;
124+
}
125+
final String[] hostWeightPairs = hostWeightMapString.split(",");
126+
for (final String hostWeightPair : hostWeightPairs) {
127+
final Matcher matcher = HOST_WEIGHT_PAIRS_PATTERN.matcher(hostWeightPair);
128+
if (!matcher.matches()) {
129+
throw new SQLException(Messages.get("HostSelector.weightedRandomInvalidHostWeightPairs"));
130+
}
131+
132+
final String hostName = matcher.group("host").trim();
133+
final String hostWeight = matcher.group("weight").trim();
134+
if (hostName.isEmpty() || hostWeight.isEmpty()) {
135+
throw new SQLException(Messages.get("HostSelector.weightedRandomInvalidHostWeightPairs"));
136+
}
137+
138+
try {
139+
final int weight = Integer.parseInt(hostWeight);
140+
if (weight < DEFAULT_WEIGHT) {
141+
throw new SQLException(Messages.get("HostSelector.weightedRandomInvalidHostWeightPairs"));
142+
}
143+
hostWeightMap.put(hostName, weight);
144+
} catch (NumberFormatException e) {
145+
throw new SQLException(Messages.get("HostSelector.roundRobinInvalidHostWeightPairs"));
146+
}
147+
}
148+
this.cachedHostWeightMap = hostWeightMap;
149+
this.cachedHostWeightMapString = hostWeightMapString;
150+
return hostWeightMap;
151+
} finally {
152+
lock.unlock();
153+
}
154+
}
155+
156+
private static class NumberRange {
157+
private int start;
158+
private int end;
159+
160+
public NumberRange(int start, int end) {
161+
this.start = start;
162+
this.end = end;
163+
}
164+
165+
public boolean isInRange(int value) {
166+
return start <= value && value <= end;
167+
}
168+
}
169+
}

wrapper/src/main/java/software/amazon/jdbc/plugin/limitless/LimitlessQueryHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ protected HostSpec createHost(final ResultSet resultSet, final int hostPortToMap
9696
final String hostName = resultSet.getString(1);
9797
final float cpu = resultSet.getFloat(2);
9898

99-
long weight = Math.round(10 - cpu * 10);
99+
long weight = (long) (10 - Math.floor(10 * cpu));
100100

101101
if (weight < 1 || weight > 10) {
102102
weight = 1; // default to 1

wrapper/src/main/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterServiceImpl.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@
3434
import software.amazon.jdbc.HostSpec;
3535
import software.amazon.jdbc.PluginService;
3636
import software.amazon.jdbc.PropertyDefinition;
37-
import software.amazon.jdbc.RoundRobinHostSelector;
37+
import software.amazon.jdbc.WeightedRandomHostSelector;
3838
import software.amazon.jdbc.hostavailability.HostAvailability;
3939
import software.amazon.jdbc.util.FullServicesContainer;
40+
import software.amazon.jdbc.util.HostSelectorUtils;
4041
import software.amazon.jdbc.util.Messages;
4142
import software.amazon.jdbc.util.Utils;
4243
import software.amazon.jdbc.util.monitoring.MonitorErrorResponse;
@@ -126,15 +127,15 @@ public void establishConnection(final LimitlessConnectionContext context) throws
126127
return;
127128
}
128129

129-
RoundRobinHostSelector.setRoundRobinHostWeightPairsProperty(
130+
HostSelectorUtils.setHostWeightPairsProperty(WeightedRandomHostSelector.WEIGHTED_RANDOM_HOST_WEIGHT_PAIRS,
130131
context.getProps(),
131132
context.getLimitlessRouters());
132133
HostSpec selectedHostSpec;
133134
try {
134135
selectedHostSpec = this.pluginService.getHostSpecByStrategy(
135136
context.getLimitlessRouters(),
136137
HostRole.WRITER,
137-
RoundRobinHostSelector.STRATEGY_ROUND_ROBIN);
138+
WeightedRandomHostSelector.STRATEGY_WEIGHTED_RANDOM);
138139
LOGGER.fine(Messages.get(
139140
"LimitlessRouterServiceImpl.selectedHost",
140141
new Object[] {selectedHostSpec != null ? selectedHostSpec.getHost() : "null"}));

0 commit comments

Comments
 (0)