Skip to content

feat(net): add lookUpIp to get InetAddress by domain#130

Open
317787106 wants to merge 8 commits intotronprotocol:release-v2.2.8from
317787106:feature/imporove_dns_lookup
Open

feat(net): add lookUpIp to get InetAddress by domain#130
317787106 wants to merge 8 commits intotronprotocol:release-v2.2.8from
317787106:feature/imporove_dns_lookup

Conversation

@317787106
Copy link
Copy Markdown

@317787106 317787106 commented Apr 1, 2026

What does this PR do?

  • add lookUpIp to get InetAddress by domain
  • ignore sources and javadoc checksum verification

Why are these changes required?

This PR has been tested by:

  • Unit Tests
  • Manual Testing

Follow up

Extra details

@317787106 317787106 changed the title feat(net): prefer to use the system's default resolver feat(net): add lookUpIp to get InetAddress by domain Apr 14, 2026
Copy link
Copy Markdown
Contributor

@xxo1shine xxo1shine left a comment

Choose a reason for hiding this comment

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

Reviewed lookUpIp implementation and test coverage. Found a few issues worth addressing before merge — details inline below.

Comment thread src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java Outdated
Comment thread src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java
Comment thread src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java
long start = System.currentTimeMillis();
for (int times = 0; times < maxRetryTimes; times++) {
String dns = publicDns[random.nextInt(publicDns.length)];
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Important] Sampling with replacement — retries can hit the same server repeatedly

random.nextInt(publicDns.length) samples with replacement, so all maxRetryTimes iterations may pick the same failing DNS server. Consider shuffling once and iterating through distinct entries:

List<String> shuffled = new ArrayList<>(Arrays.asList(publicDns));
Collections.shuffle(shuffled, random);
for (String dns : shuffled.subList(0, Math.min(maxRetryTimes, shuffled.size()))) {
    // existing retry body
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It has little impact. so ignore it.


@Test
public void testLookUpIp_localhost_ipv6_resolvesViaHosts() {
InetAddress address = LookUpTxt.lookUpIp("localhost", false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Minor] IPv6 localhost test may be unreliable in some CI environments

Not all Docker/CI containers configure an IPv6 loopback (::1). This test may fail in restricted environments. Consider adding a precondition check:

Assume.assumeTrue("IPv6 not available", isIPv6Available());

or move it to a separate integration-test source set.

*/
@Test
public void testLookUpIp_wellKnownDomain_ipv4_returnsNonNull() {
InetAddress address = LookUpTxt.lookUpIp("example.com", true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Minor] Network-dependent test — should be marked as an integration test

This test issues a real DNS query to example.com and will fail in offline or network-restricted CI environments (e.g. sandboxed build agents). Consider annotating with @Category(IntegrationTest.class) or moving it to a dedicated integration-test source set so it can be excluded from the standard unit-test run.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I agree with @xxo1shine, as it will be a unstable test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Nodes with restricted network access can use this unit test to verify network connectivity, without needing to pass all unit tests.

Comment thread src/test/java/org/tron/p2p/dns/LookUpTxtTest.java Outdated
Copy link
Copy Markdown

@Little-Peony Little-Peony left a comment

Choose a reason for hiding this comment

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

Operator precedence — missing parentheses (line 129)

The mixed && / || without explicit grouping makes the intent ambiguous and relies on operator precedence that readers must mentally verify:

// current
if (useIPv4 && addr instanceof Inet4Address
    || !useIPv4 && addr instanceof Inet6Address) {

Suggestion — add parentheses to make each AND-group self-contained:

if ((useIPv4 && addr instanceof Inet4Address)
    || (!useIPv4 && addr instanceof Inet6Address)) {

This is functionally equivalent but removes any ambiguity for future readers.

Comment thread src/main/java/org/tron/p2p/dns/lookup/LookUpTxt.java Outdated
public void testLookUpIp_nonexistentDomain_returnsNull() throws Exception {
// Limit public-DNS retries to 1 via reflection so the test exits quickly.
java.lang.reflect.Field field = LookUpTxt.class.getDeclaredField("maxRetryTimes");
field.setAccessible(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reflection on a string literal "maxRetryTimes" creates a silent breakage risk: if the field is renamed, this throws NoSuchFieldException at runtime rather than a compile-time error.

Consider exposing a package-private or @VisibleForTesting setter, or extracting the constant to a named field in the test so at least the string appears in one place:

private static final String MAX_RETRY_FIELD = "maxRetryTimes";
// then: LookUpTxt.class.getDeclaredField(MAX_RETRY_FIELD)

Better yet, if maxRetryTimes is made package-private or @VisibleForTesting, the test can set it directly without reflection.

@Test
public void testLookUpIp_nonexistentDomain_returnsNull() throws Exception {
// Limit public-DNS retries to 1 via reflection so the test exits quickly.
java.lang.reflect.Field field = LookUpTxt.class.getDeclaredField("maxRetryTimes");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Setting maxRetryTimes = 1 only caps Step 2 (public DNS, already has a 1 s SimpleResolver timeout). Step 1InetAddress.getAllByName() — is governed by the OS resolver and can block for 5–30 s on a non-existent domain in some CI environments, making the test slow or flaky.

Two options:

Option A — short-term safety net: add a timeout bound so the test fails fast instead of hanging:

@Test(timeout = 5_000)
public void testLookUpIp_nonexistentDomain_returnsNull() throws Exception {

Option B — proper fix: extract the OS-resolver step behind an injectable interface (e.g. Function<String, InetAddress[]> osResolver) so tests can stub it to throw UnknownHostException immediately, eliminating the real network call entirely.

} catch (TimeoutException e) {
future.cancel(true);
log.debug("OS name resolver timed out for {}", domain);
} catch (Exception e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

catch (Exception e) is too broad — it silently swallows InterruptedException and ExecutionException alongside UnknownHostException. Consider handling each explicitly:

} catch (ExecutionException e) {
    log.debug("OS name resolver failed for {}: {}", domain, e.getCause().getMessage());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // restore interrupt flag
    log.debug("OS name resolver interrupted for {}", domain);
}

Copy link
Copy Markdown
Contributor

@xxo1shine xxo1shine left a comment

Choose a reason for hiding this comment

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

[High] ExecutorService has no shutdown mechanism

OS_RESOLVER_EXECUTOR is a JVM-global static resource with no shutdown() call or shutdown hook:

static final ExecutorService OS_RESOLVER_EXECUTOR = Executors.newCachedThreadPool(...);

newCachedThreadPool creates threads on demand with no upper bound. Under heavy or frequent calls this can grow unbounded. Suggestions:

Copy link
Copy Markdown
Contributor

@xxo1shine xxo1shine left a comment

Choose a reason for hiding this comment

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

[High] future.cancel(true) has no effect on InetAddress.getAllByName, leading to thread leaks

InetAddress.getAllByName is a blocking native call that does not respond to thread interrupts. After future.cancel(true) the thread keeps running until the OS-level resolution finishes on its own:

} catch (TimeoutException e) {
    future.cancel(true);  // interrupt is ignored by the native call
    log.debug("OS name resolver timed out for {}", domain);
}

In scenarios where OS DNS resolution stalls (network issues, slow DNS server, etc.), every timeout leaves a thread holding a pool slot. Combined with newCachedThreadPool, this can cause the thread count to grow without bound.

At a minimum, add a comment to document the known limitation:

} catch (TimeoutException e) {
    // cancel(true) sends an interrupt, but InetAddress.getAllByName() is a
    // native blocking call and does NOT respond to interrupts. The thread
    // will keep running until the OS-level resolution completes or times out.
    // This is an accepted limitation of wrapping non-interruptible I/O in a Future.
    future.cancel(true);
    log.debug("OS name resolver timed out for {}", domain);
}

For a stronger guarantee, consider using a plain daemon Thread instead of a pool, so the JVM can reclaim it on exit without relying on pool eviction.

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.

4 participants