Skip to content

Commit 94c8795

Browse files
tolbertamolim7t
authored andcommitted
JAVA-1832: Add Ec2MultiRegionAddressTranslator
1 parent e60c803 commit 94c8795

File tree

4 files changed

+277
-4
lines changed

4 files changed

+277
-4
lines changed

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### 4.0.0-alpha4 (in progress)
66

7+
- [new feature] JAVA-1832: Add Ec2MultiRegionAddressTranslator
78
- [improvement] JAVA-1825: Add remaining Typesafe config primitive types to DriverConfigProfile
89
- [new feature] JAVA-1846: Add ConstantReconnectionPolicy
910
- [improvement] JAVA-1824: Make policies overridable in profiles
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright DataStax, Inc.
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+
package com.datastax.oss.driver.internal.core.addresstranslation;
17+
18+
import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator;
19+
import com.datastax.oss.driver.api.core.context.DriverContext;
20+
import com.datastax.oss.driver.internal.core.util.Loggers;
21+
import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting;
22+
import java.net.InetAddress;
23+
import java.net.InetSocketAddress;
24+
import java.util.Enumeration;
25+
import java.util.Hashtable;
26+
import javax.naming.Context;
27+
import javax.naming.NamingEnumeration;
28+
import javax.naming.NamingException;
29+
import javax.naming.directory.Attribute;
30+
import javax.naming.directory.Attributes;
31+
import javax.naming.directory.DirContext;
32+
import javax.naming.directory.InitialDirContext;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
36+
/**
37+
* {@link AddressTranslator} implementation for a multi-region EC2 deployment <b>where clients are
38+
* also deployed in EC2</b>.
39+
*
40+
* <p>Its distinctive feature is that it translates addresses according to the location of the
41+
* Cassandra host:
42+
*
43+
* <ul>
44+
* <li>addresses in different EC2 regions (than the client) are unchanged;
45+
* <li>addresses in the same EC2 region are <b>translated to private IPs</b>.
46+
* </ul>
47+
*
48+
* This optimizes network costs, because Amazon charges more for communication over public IPs.
49+
*
50+
* <p>Implementation note: this class performs a reverse DNS lookup of the origin address, to find
51+
* the domain name of the target instance. Then it performs a forward DNS lookup of the domain name;
52+
* the EC2 DNS does the private/public switch automatically based on location.
53+
*/
54+
public class Ec2MultiRegionAddressTranslator implements AddressTranslator {
55+
56+
private static final Logger LOG = LoggerFactory.getLogger(Ec2MultiRegionAddressTranslator.class);
57+
58+
private final DirContext ctx;
59+
private final String logPrefix;
60+
61+
public Ec2MultiRegionAddressTranslator(@SuppressWarnings("unused") DriverContext context) {
62+
this.logPrefix = context.sessionName();
63+
@SuppressWarnings("JdkObsolete")
64+
Hashtable<Object, Object> env = new Hashtable<>();
65+
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
66+
try {
67+
ctx = new InitialDirContext(env);
68+
} catch (NamingException e) {
69+
throw new RuntimeException("Could not create translator", e);
70+
}
71+
}
72+
73+
@VisibleForTesting
74+
Ec2MultiRegionAddressTranslator(DirContext ctx) {
75+
this.logPrefix = "test";
76+
this.ctx = ctx;
77+
}
78+
79+
@Override
80+
public InetSocketAddress translate(InetSocketAddress socketAddress) {
81+
InetAddress address = socketAddress.getAddress();
82+
try {
83+
// InetAddress#getHostName() is supposed to perform a reverse DNS lookup, but for some reason
84+
// it doesn't work within the same EC2 region (it returns the IP address itself).
85+
// We use an alternate implementation:
86+
String domainName = lookupPtrRecord(reverse(address));
87+
if (domainName == null) {
88+
LOG.warn("[{}] Found no domain name for {}, returning it as-is", logPrefix, address);
89+
return socketAddress;
90+
}
91+
92+
InetAddress translatedAddress = InetAddress.getByName(domainName);
93+
LOG.debug("[{}] Resolved {} to {}", logPrefix, address, translatedAddress);
94+
return new InetSocketAddress(translatedAddress, socketAddress.getPort());
95+
} catch (Exception e) {
96+
Loggers.warnWithException(
97+
LOG, "[{}] Error resolving {}, returning it as-is", logPrefix, address, e);
98+
return socketAddress;
99+
}
100+
}
101+
102+
private String lookupPtrRecord(String reversedDomain) throws Exception {
103+
Attributes attrs = ctx.getAttributes(reversedDomain, new String[] {"PTR"});
104+
for (NamingEnumeration ae = attrs.getAll(); ae.hasMoreElements(); ) {
105+
Attribute attr = (Attribute) ae.next();
106+
Enumeration<?> vals = attr.getAll();
107+
if (vals.hasMoreElements()) {
108+
return vals.nextElement().toString();
109+
}
110+
}
111+
return null;
112+
}
113+
114+
@Override
115+
public void close() {
116+
try {
117+
ctx.close();
118+
} catch (NamingException e) {
119+
Loggers.warnWithException(LOG, "Error closing translator", e);
120+
}
121+
}
122+
123+
// Builds the "reversed" domain name in the ARPA domain to perform the reverse lookup
124+
@VisibleForTesting
125+
static String reverse(InetAddress address) {
126+
byte[] bytes = address.getAddress();
127+
if (bytes.length == 4) return reverseIpv4(bytes);
128+
else return reverseIpv6(bytes);
129+
}
130+
131+
private static String reverseIpv4(byte[] bytes) {
132+
StringBuilder builder = new StringBuilder();
133+
for (int i = bytes.length - 1; i >= 0; i--) {
134+
builder.append(bytes[i] & 0xFF).append('.');
135+
}
136+
builder.append("in-addr.arpa");
137+
return builder.toString();
138+
}
139+
140+
private static String reverseIpv6(byte[] bytes) {
141+
StringBuilder builder = new StringBuilder();
142+
for (int i = bytes.length - 1; i >= 0; i--) {
143+
byte b = bytes[i];
144+
int lowNibble = b & 0x0F;
145+
int highNibble = b >> 4 & 0x0F;
146+
builder
147+
.append(Integer.toHexString(lowNibble))
148+
.append('.')
149+
.append(Integer.toHexString(highNibble))
150+
.append('.');
151+
}
152+
builder.append("ip6.arpa");
153+
return builder.toString();
154+
}
155+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright DataStax, Inc.
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+
package com.datastax.oss.driver.internal.core.addresstranslation;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.Mockito.any;
20+
import static org.mockito.Mockito.anyString;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.times;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
26+
import java.net.InetAddress;
27+
import java.net.InetSocketAddress;
28+
import javax.naming.NamingException;
29+
import javax.naming.directory.BasicAttributes;
30+
import javax.naming.directory.InitialDirContext;
31+
import org.junit.Test;
32+
33+
public class Ec2MultiRegionAddressTranslatorTest {
34+
35+
@Test
36+
public void should_return_same_address_when_no_entry_found() throws Exception {
37+
InitialDirContext mock = mock(InitialDirContext.class);
38+
when(mock.getAttributes(anyString(), any(String[].class))).thenReturn(new BasicAttributes());
39+
Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock);
40+
41+
InetSocketAddress address = new InetSocketAddress("192.0.2.5", 9042);
42+
assertThat(translator.translate(address)).isEqualTo(address);
43+
}
44+
45+
@Test
46+
public void should_return_same_address_when_exception_encountered() throws Exception {
47+
InitialDirContext mock = mock(InitialDirContext.class);
48+
when(mock.getAttributes(anyString(), any(String[].class)))
49+
.thenThrow(new NamingException("Problem resolving address (not really)."));
50+
Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock);
51+
52+
InetSocketAddress address = new InetSocketAddress("192.0.2.5", 9042);
53+
assertThat(translator.translate(address)).isEqualTo(address);
54+
}
55+
56+
@Test
57+
public void should_return_new_address_when_match_found() throws Exception {
58+
InetSocketAddress expectedAddress = new InetSocketAddress("54.32.55.66", 9042);
59+
60+
InitialDirContext mock = mock(InitialDirContext.class);
61+
when(mock.getAttributes("5.2.0.192.in-addr.arpa", new String[] {"PTR"}))
62+
.thenReturn(new BasicAttributes("PTR", expectedAddress.getHostName()));
63+
Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock);
64+
65+
InetSocketAddress address = new InetSocketAddress("192.0.2.5", 9042);
66+
assertThat(translator.translate(address)).isEqualTo(expectedAddress);
67+
}
68+
69+
@Test
70+
public void should_close_context_when_closed() throws Exception {
71+
InitialDirContext mock = mock(InitialDirContext.class);
72+
Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock);
73+
74+
// ensure close has not been called to this point.
75+
verify(mock, times(0)).close();
76+
translator.close();
77+
// ensure close is closed.
78+
verify(mock).close();
79+
}
80+
81+
@Test
82+
public void should_build_reversed_domain_name_for_ip_v4() throws Exception {
83+
InetAddress address = InetAddress.getByName("192.0.2.5");
84+
assertThat(Ec2MultiRegionAddressTranslator.reverse(address))
85+
.isEqualTo("5.2.0.192.in-addr.arpa");
86+
}
87+
88+
@Test
89+
public void should_build_reversed_domain_name_for_ip_v6() throws Exception {
90+
InetAddress address = InetAddress.getByName("2001:db8::567:89ab");
91+
assertThat(Ec2MultiRegionAddressTranslator.reverse(address))
92+
.isEqualTo("b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa");
93+
}
94+
}

manual/core/address_resolution/README.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ translation. Write a class that implements [AddressTranslator] with the followin
6262

6363
```java
6464
public class MyAddressTranslator implements AddressTranslator {
65-
65+
6666
public PassThroughAddressTranslator(DriverContext context, DriverOption configRoot) {
6767
// retrieve any required dependency or extra configuration option, otherwise can stay empty
6868
}
@@ -71,7 +71,7 @@ public class MyAddressTranslator implements AddressTranslator {
7171
public InetSocketAddress translate(InetSocketAddress address) {
7272
// your custom translation logic
7373
}
74-
74+
7575
@Override
7676
public void close() {
7777
// free any resources if needed, otherwise can stay empty
@@ -83,15 +83,38 @@ Then reference this class from the [configuration](../configuration/):
8383

8484
```
8585
datastax-java-driver.address-translator.class = com.mycompany.MyAddressTranslator
86-
```
86+
```
8787

8888
Note: the contact points provided while creating the `CqlSession` are not translated, only addresses
8989
retrieved from or sent by Cassandra nodes are.
9090

91-
<!-- TODO ec2 multi-region translator -->
91+
### EC2 multi-region
92+
93+
If you deploy both Cassandra and client applications on Amazon EC2, and your cluster spans multiple regions, you'll have
94+
to configure your Cassandra nodes to broadcast public RPC addresses.
95+
96+
However, this is not always the most cost-effective: if a client and a node are in the same region, it would be cheaper
97+
to connect over the private IP. Ideally, you'd want to pick the best address in each case.
98+
99+
The driver provides [Ec2MultiRegionAddressTranslator] which does exactly that. To use it, specify the following in
100+
the [configuration](../configuration/):
101+
102+
```
103+
datastax-java-driver.address-translator.class = Ec2MultiRegionAddressTranslator
104+
```
105+
106+
With this configuration, you keep broadcasting public RPC addresses. But each time the driver connects to a new
107+
Cassandra node:
108+
109+
* if the node is *in the same EC2 region*, the public IP will be translated to the intra-region private IP;
110+
* otherwise, it will not be translated.
92111

112+
(To achieve this, `Ec2MultiRegionAddressTranslator` performs a reverse DNS lookup of the origin address, to find the
113+
domain name of the target instance. Then it performs a forward DNS lookup of the domain name; the EC2 DNS does the
114+
private/public switch automatically based on location).
93115

94116
[AddressTranslator]: http://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/api/core/addresstranslation/AddressTranslator.html
117+
[Ec2MultiRegionAddressTranslator]: http://docs.datastax.com/en/drivers/java/4.0/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslator.html
95118

96119
[cassandra.yaml]: https://docs.datastax.com/en/cassandra/3.x/cassandra/configuration/configCassandra_yaml.html
97120
[rpc_address]: https://docs.datastax.com/en/cassandra/3.x/cassandra/configuration/configCassandra_yaml.html?scroll=configCassandra_yaml__rpc_address

0 commit comments

Comments
 (0)