feat(net): add lookUpIp to get InetAddress by domain#130
feat(net): add lookUpIp to get InetAddress by domain#130317787106 wants to merge 8 commits intotronprotocol:release-v2.2.8from
Conversation
xxo1shine
left a comment
There was a problem hiding this comment.
Reviewed lookUpIp implementation and test coverage. Found a few issues worth addressing before merge — details inline below.
| long start = System.currentTimeMillis(); | ||
| for (int times = 0; times < maxRetryTimes; times++) { | ||
| String dns = publicDns[random.nextInt(publicDns.length)]; | ||
| try { |
There was a problem hiding this comment.
[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
}There was a problem hiding this comment.
It has little impact. so ignore it.
|
|
||
| @Test | ||
| public void testLookUpIp_localhost_ipv6_resolvesViaHosts() { | ||
| InetAddress address = LookUpTxt.lookUpIp("localhost", false); |
There was a problem hiding this comment.
[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); |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
I agree with @xxo1shine, as it will be a unstable test.
There was a problem hiding this comment.
Nodes with restricted network access can use this unit test to verify network connectivity, without needing to pass all unit tests.
Little-Peony
left a comment
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
Setting maxRetryTimes = 1 only caps Step 2 (public DNS, already has a 1 s SimpleResolver timeout). Step 1 — InetAddress.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) { |
There was a problem hiding this comment.
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);
}There was a problem hiding this comment.
[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:
There was a problem hiding this comment.
[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.
What does this PR do?
Why are these changes required?
This PR has been tested by:
Follow up
Extra details