Skip to content

Conversation

@arturobernalg
Copy link
Member

Introduce Rfc6724AddressSelectingDnsResolver and unit tests.
Define INTERLEAVE as “no bias” (preserve RFC6724 order); keep ONLY/PREFER behavior unchanged.

Define INTERLEAVE as no-bias and align tests and debug output.
Copy link
Contributor

@rschmitt rschmitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main thing here is that I'd like to see a proper implementation of INTERLEAVE, more unit test coverage, and an example program we can use for manual testing.

Comment on lines 470 to 473
final InetAddress DA = a.dst;
final InetAddress DB = b.dst;
final InetAddress SourceDA = a.src;
final InetAddress SourceDB = b.src;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be named something like aDst, bDst, aSrc, and aDst respectively, so it doesn't look like you're calling static methods later on

if (ip.isLinkLocalAddress()) {
return Scope.LINK_LOCAL;
}
if (ip.isMulticastAddress()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're writing this for use in an HTTP client, not as a general-purpose resolver (e.g. JEP 418), and I'm pretty sure TCP connections can only be established to unicast addresses. Should we just filter out multicast addresses?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be dead code now, right? Don't these get filtered out now before this method is called?

InetAddress src = null;
try (final DatagramSocket s = new DatagramSocket()) {
s.connect(dest); // does not send packets; OS picks source addr/if
src = s.getLocalAddress();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to inject a thing here that you can mock for unit testing purposes, e.g. a Function<InetSocketAddress, InetAddress> (or an equivalent that throws IOException).

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class Rfc6724AddressSelectingDnsResolverTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need more unit test coverage here. I'd also like to see an integration test or a runnable example (with debug/trace logging enabled) that resolves a given DNS name. That would afford some non-trivial manual testing using loopback addresses, /etc/hosts, Docker networking, etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need any additional test coverage for the prefix matching stuff? That seems like quite a bit of non-trivial code

Comment on lines 233 to 246
final boolean preferV6 = pref == ProtocolFamilyPreference.PREFER_IPV6;
final List<InetAddress> first = new ArrayList<>();
final List<InetAddress> second = new ArrayList<>();
for (final InetAddress a : rfcSorted) {
final boolean isV6 = a instanceof Inet6Address;
if (preferV6 && isV6 || !preferV6 && !isV6) {
first.add(a);
} else {
second.add(a);
}
}
final List<InetAddress> merged = new ArrayList<>(rfcSorted.size());
merged.addAll(first);
merged.addAll(second);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're already relying on the JDK to provide a stable sort, you can write this as:

List<InetAddress> merged =
    rfcSorted.stream()
        .sorted(Comparator.comparing(
            a -> ((a instanceof Inet6Address) != preferV6)
        ))
        .toList();

Add manual gated IT to dump DEFAULT vs INTERLEAVE results.
Expand unit coverage for scope mapping and core RFC comparison rules.
@arturobernalg
Copy link
Member Author

@rschmitt Please do another pass. I think i solve all your remarks

Comment on lines +48 to +49
assumeTrue(Boolean.getBoolean(PROP_ENABLE),
"Enable with -Dhttpclient.rfc6724.it=true");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather control this with JUnit 5 tags somehow. The advantage of what you've done here is that we don't have to define an entire Maven profile for this test, but we could add support in our build for controlling test selection via system properties.

* @since 5.7
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public final class Rfc6724AddressSelectingDnsResolver implements DnsResolver {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not crazy about the name -- for one thing, it's obviously cryptic; for another, this RFC could be superceded by a newer one (as has happened multiple times with Happy Eyeballs and HTTP itself), and we'll be stuck with the old RFC number as part of our public API symbols

Comment on lines +125 to +127
if (LOG.isDebugEnabled()) {
LOG.debug("{} resolved '{}' -> null", simpleName(), host);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pass simpleName() here? The logger configuration should add the class name

if (LOG.isDebugEnabled()) {
LOG.debug("{} resolved '{}' -> []", simpleName(), host);
}
return new InetAddress[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why an empty array here, but null on line 128?

Comment on lines +185 to +187
private static List<InetAddress> filterCandidates(
final InetAddress[] resolved,
final ProtocolFamilyPreference pref) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be nicely implemented using streaming operations: Arrays.stream(resolved).filter(a -> isUsableDestination(a)).filter(pref::shouldInclude).collect(toList())

Comment on lines +354 to +363
int i = 0;
int j = 0;
while (i < first.size() || j < second.size()) {
if (i < first.size()) {
out.add(first.get(i++));
}
if (j < second.size()) {
out.add(second.get(j++));
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a little cleaner with Iterators. It saves you from having to index into arrays (I'm a little neurotic about that, I realize), and it may help optimize out runtime array bounds checks.

import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.config.ProtocolFamilyPreference;

public final class Rfc6724ResolverExample {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/hc/client # ./run-example.sh Rfc6724ResolverExample www.amazon.com
Host: www.amazon.com
Preference: DEFAULT
  IPv6 2600:1409:9800:1680:0:0:0:3bd4
  IPv6 2600:1409:9800:168d:0:0:0:3bd4
  IPv4 184.27.192.135

src/hc/client # ./run-example.sh Rfc6724ResolverExample www.amazon.com INTERLEAVE
Host: www.amazon.com
Preference: INTERLEAVE
  IPv6 2600:1409:9800:168d:0:0:0:3bd4
  IPv4 184.27.192.135
  IPv6 2600:1409:9800:1680:0:0:0:3bd4

Neat! I'd be cool if I could see the before-and-after (maybe you could intercept the "before" by wrapping the system default resolver, then passing the wrapper in to the RFC 6724 resolver), but it's not essential.

Comment on lines +307 to +314
final Method addr = resolverClass.getDeclaredMethod("addr", InetAddress.class);
addr.setAccessible(true);

final Method fmtArr = resolverClass.getDeclaredMethod("fmt", InetAddress[].class);
fmtArr.setAccessible(true);

final Method fmtList = resolverClass.getDeclaredMethod("fmt", List.class);
fmtList.setAccessible(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just declare these methods package-private and call them directly!

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class Rfc6724AddressSelectingDnsResolverTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need any additional test coverage for the prefix matching stuff? That seems like quite a bit of non-trivial code

import org.apache.hc.client5.http.config.ProtocolFamilyPreference;
import org.junit.jupiter.api.Test;

class ManualRfc6724ResolverIT {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this actually test? There are no assertions. If this basically just repeats the examples class then I'd rather not add it.

@rschmitt
Copy link
Contributor

There are some things from my first review that still haven't been addressed. I suggest going through the comments one by one and hitting "Resolve conversation" as you address each one, so you can track what is and isn't done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants