From 265adacdf7202e65632e28c3137adfed5175f45c Mon Sep 17 00:00:00 2001 From: LinZong Date: Mon, 7 Oct 2024 17:41:39 +0800 Subject: [PATCH 1/2] 1. Fix 'initialRequest' guard might be incorrect if the initial value of 'idleConnectionTimeout' is set to a value larger than current (and later for a period) System.nanoTime(). 2. Fix race condition on setting 'lastRequest' timestamp among concurrent requests, ensuring its value is always be monotonic. --- src/main/java/org/xbill/DNS/DohResolver.java | 48 +++++++++- .../java/org/xbill/DNS/DohResolverTest.java | 95 ++++++++++++++++++- 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/xbill/DNS/DohResolver.java b/src/main/java/org/xbill/DNS/DohResolver.java index 0933ef18..51d865ae 100644 --- a/src/main/java/org/xbill/DNS/DohResolver.java +++ b/src/main/java/org/xbill/DNS/DohResolver.java @@ -23,6 +23,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import javax.net.ssl.HttpsURLConnection; @@ -91,6 +92,8 @@ public final class DohResolver implements Resolver { private final AsyncSemaphore maxConcurrentRequests; private final AtomicLong lastRequest = new AtomicLong(0); + + private final AtomicBoolean initialRequestSentMark = new AtomicBoolean(false); private final AsyncSemaphore initialRequestLock = new AsyncSemaphore(1); private static final String APPLICATION_DNS_MESSAGE = "application/dns-message"; @@ -469,15 +472,28 @@ private CompletionStage sendAsync11(final Message query, Executor execu .thenCompose(Function.identity()); } + /** + * Check whether current initiating DoH request is initial request of this {@link DohResolver}. + */ + private boolean checkInitialRequest() { + // If initial request haven't been completed successfully yet, just return true. + if (!initialRequestSentMark.get()) { + return true; + } + + // Otherwise, check whether such request is happened + // after last successful request plus idle connection timeout. + long lastRequestTime = lastRequest.get(); + return (lastRequestTime + idleConnectionTimeout.toNanos() < System.nanoTime()); + } + private CompletionStage sendAsync11WithInitialRequestPermit( Message query, Executor executor, long startTime, Object requestBuilder, Permit initialRequestPermit) { - long lastRequestTime = lastRequest.get(); - boolean isInitialRequest = - (lastRequestTime < System.nanoTime() - idleConnectionTimeout.toNanos()); + boolean isInitialRequest = checkInitialRequest(); if (!isInitialRequest) { initialRequestPermit.release(); } @@ -516,6 +532,25 @@ private CompletionStage sendAsync11WithInitialRequestPermit( .thenCompose(Function.identity()); } + /** + * Set last request time to {@link DohResolver#lastRequest}, which ensures only the largest timestamp could be accepted. + * + * @param startTime start time in nanos of a Doh request. + */ + private void setLastRequestTime(long startTime) { + long current = lastRequest.get(); + // Only update value of 'lastRequest' if timestamp in 'lastRequest' is smaller than incoming 'startTime' value. + if (current < startTime) { + while (!lastRequest.compareAndSet(current, startTime)) { + // CAS failed, re-verify the eligibility of timestamp in 'lastRequest' to be updated to the incoming 'startTime' value. + current = lastRequest.get(); + if (current > startTime) { + return; + } + } + } + } + private CompletionStage sendAsync11WithConcurrentRequestPermit( Message query, Executor executor, @@ -548,7 +583,12 @@ private CompletionStage sendAsync11WithConcurrentRequestPermit( .whenComplete( (result, ex) -> { if (ex == null) { - lastRequest.set(startTime); + setLastRequestTime(startTime); + if (isInitialRequest) { + // initial request was completed successfully, so toggle initialRequestSentMark to true. + // it's very safe to toggle initialRequestSentMark here, since this code had been guarded by initialRequestLock and its permit outside. + initialRequestSentMark.compareAndSet(false, true); + } } maxConcurrentRequestPermit.release(); if (isInitialRequest) { diff --git a/src/test/java/org/xbill/DNS/DohResolverTest.java b/src/test/java/org/xbill/DNS/DohResolverTest.java index c3931d63..98f489c1 100644 --- a/src/test/java/org/xbill/DNS/DohResolverTest.java +++ b/src/test/java/org/xbill/DNS/DohResolverTest.java @@ -1,9 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import io.netty.handler.codec.http.HttpHeaderNames; import io.vertx.core.Future; import io.vertx.core.Vertx; @@ -20,7 +17,9 @@ import java.time.Duration; import java.util.Base64; import java.util.Collections; +import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,6 +27,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.*; + @ExtendWith(VertxExtension.class) class DohResolverTest { private DohResolver resolver; @@ -152,6 +153,88 @@ void initialRequestSlowResolve(Vertx vertx, VertxTestContext context) { }); } + + @Test + void initialRequestGuardIfIdleConnectionTimeIsLargerThanSystemNanoTime(Vertx vertx, VertxTestContext context) { + if (isPreJava9()) { + System.out.println("Current JVM is PreJava9, no need to run such test."); + context.completeNow(); + return; + } + resolver = new DohResolver("http://localhost", + 2, + // so long idleConnectionTimeout + // in order to hack the condition for checking initial request in org.xbill.DNS.DohResolver.checkInitialRequest + Duration.ofNanos(System.nanoTime() + Duration.ofSeconds(100L).toNanos())); + resolver.setTimeout(Duration.ofSeconds(1)); + // Just add a 100ms delay before responding to the 1st call + // to simulate a 'concurrent doh request' for the 2nd call, + // then let the fake dns server respond to the 2nd call ASAP. + allRequestsUseTimeout = false; + + // idleConnectionTimeout = 2s, lastRequest = 0L + // Ensure lastRequest + idleConnectionTimeout < System.nanoTime() (3s) + + // Timeline: + // |<-------- 100ms -------->| + // ↑ ↑ + // 1st call sent response of 1st call + // |20ms|<------ 80ms ------>|<------ few millis ------->| + // ↑ wait until 1st call ↑ ↑ + // 2nd call begin 2nd call sent response of 2nd call + + AtomicBoolean firstCallCompleted = new AtomicBoolean(false); + + setupResolverWithServer(Duration.ofMillis(100L), + 200, + 2, + vertx, + context) + .onSuccess( + server -> { + // First call + CompletionStage firstCall = resolver.sendAsync(qm); + // Ensure second call was made after first call. + sleepNotThrown(20L); + CompletionStage secondCall = resolver.sendAsync(qm); + + Future.fromCompletionStage(firstCall) + .onComplete( + context.succeeding( + result -> + context.verify( + () -> { + assertEquals(Rcode.NOERROR, result.getHeader().getRcode()); + assertEquals(0, result.getHeader().getID()); + assertEquals(queryName, result.getQuestion().getName()); + firstCallCompleted.set(true); + }))); + + Future.fromCompletionStage(secondCall) + .onComplete( + context.succeeding( + result -> + context.verify( + () -> { + assertTrue(firstCallCompleted.get()); + assertEquals(Rcode.NOERROR, result.getHeader().getRcode()); + assertEquals(0, result.getHeader().getID()); + assertEquals(queryName, result.getQuestion().getName()); + // Complete context after the 2nd call was completed. + context.completeNow(); + }))); + } + ); + } + + private static void sleepNotThrown(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + @Test void initialRequestTimeoutResolve(Vertx vertx, VertxTestContext context) { resolver = new DohResolver("http://localhost", 2, Duration.ofMinutes(2)); @@ -192,6 +275,10 @@ void initialRequestTimeoutResolve(Vertx vertx, VertxTestContext context) { }); } + private static boolean isPreJava9() { + return System.getProperty("java.version").startsWith("1."); + } + private Future setupResolverWithServer( Duration responseDelay, int statusCode, @@ -211,7 +298,7 @@ private Future setupServer( VertxTestContext context, Vertx vertx) { HttpVersion version = - System.getProperty("java.version").startsWith("1.") + isPreJava9() ? HttpVersion.HTTP_1_1 : HttpVersion.HTTP_2; AtomicInteger requestCount = new AtomicInteger(0); From a026ff309e42722926f4c00100ffe84533b159ab Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 5 Jan 2025 15:26:23 +0100 Subject: [PATCH 2/2] Fix initial request using recommended nanoTime calculation #345 --- src/main/java/org/xbill/DNS/DohResolver.java | 68 ++----- .../java/org/xbill/DNS/DohResolverTest.java | 169 +++++++++--------- 2 files changed, 96 insertions(+), 141 deletions(-) diff --git a/src/main/java/org/xbill/DNS/DohResolver.java b/src/main/java/org/xbill/DNS/DohResolver.java index 51d865ae..23dc871a 100644 --- a/src/main/java/org/xbill/DNS/DohResolver.java +++ b/src/main/java/org/xbill/DNS/DohResolver.java @@ -23,7 +23,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import javax.net.ssl.HttpsURLConnection; @@ -92,8 +91,6 @@ public final class DohResolver implements Resolver { private final AsyncSemaphore maxConcurrentRequests; private final AtomicLong lastRequest = new AtomicLong(0); - - private final AtomicBoolean initialRequestSentMark = new AtomicBoolean(false); private final AsyncSemaphore initialRequestLock = new AsyncSemaphore(1); private static final String APPLICATION_DNS_MESSAGE = "application/dns-message"; @@ -177,6 +174,11 @@ public final class DohResolver implements Resolver { USE_HTTP_CLIENT = initSuccess; } + // package-visible for testing + long getNanoTime() { + return System.nanoTime(); + } + /** * Creates a new DoH resolver that performs lookups with HTTP GET and the default timeout (5s). * @@ -318,7 +320,7 @@ public CompletionStage sendAsync(Message query, Executor executor) { private CompletionStage sendAsync8(final Message query, Executor executor) { byte[] queryBytes = prepareQuery(query).toWire(); String url = getUrl(queryBytes); - long startTime = System.nanoTime(); + long startTime = getNanoTime(); return maxConcurrentRequests .acquire(timeout) .handleAsync( @@ -366,7 +368,7 @@ private SendAndGetMessageBytesResponse sendAndGetMessageBytes( ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory); } - Duration remainingTimeout = timeout.minus(System.nanoTime() - startTime, ChronoUnit.NANOS); + Duration remainingTimeout = timeout.minus(getNanoTime() - startTime, ChronoUnit.NANOS); conn.setConnectTimeout((int) remainingTimeout.toMillis()); conn.setReadTimeout((int) remainingTimeout.toMillis()); conn.setRequestMethod(usePost ? "POST" : "GET"); @@ -392,7 +394,7 @@ private SendAndGetMessageBytesResponse sendAndGetMessageBytes( int offset = 0; while ((r = is.read(responseBytes, offset, responseBytes.length - offset)) > 0) { offset += r; - remainingTimeout = timeout.minus(System.nanoTime() - startTime, ChronoUnit.NANOS); + remainingTimeout = timeout.minus(getNanoTime() - startTime, ChronoUnit.NANOS); if (remainingTimeout.isNegative()) { throw new SocketTimeoutException(); } @@ -406,7 +408,7 @@ private SendAndGetMessageBytesResponse sendAndGetMessageBytes( byte[] buffer = new byte[4096]; int r; while ((r = is.read(buffer, 0, buffer.length)) > 0) { - remainingTimeout = timeout.minus(System.nanoTime() - startTime, ChronoUnit.NANOS); + remainingTimeout = timeout.minus(getNanoTime() - startTime, ChronoUnit.NANOS); if (remainingTimeout.isNegative()) { throw new SocketTimeoutException(); } @@ -435,7 +437,7 @@ private void discardStream(InputStream es) throws IOException { } private CompletionStage sendAsync11(final Message query, Executor executor) { - long startTime = System.nanoTime(); + long startTime = getNanoTime(); byte[] queryBytes = prepareQuery(query).toWire(); String url = getUrl(queryBytes); @@ -457,7 +459,7 @@ private CompletionStage sendAsync11(final Message query, Executor execu // check if this request needs to be done synchronously because of HttpClient's stupidity to // not use the connection pool for HTTP/2 until one connection is successfully established, // which could lead to hundreds of connections (and threads with the default executor) - Duration remainingTimeout = timeout.minus(System.nanoTime() - startTime, ChronoUnit.NANOS); + Duration remainingTimeout = timeout.minus(getNanoTime() - startTime, ChronoUnit.NANOS); return initialRequestLock .acquire(remainingTimeout) .handle( @@ -472,34 +474,20 @@ private CompletionStage sendAsync11(final Message query, Executor execu .thenCompose(Function.identity()); } - /** - * Check whether current initiating DoH request is initial request of this {@link DohResolver}. - */ - private boolean checkInitialRequest() { - // If initial request haven't been completed successfully yet, just return true. - if (!initialRequestSentMark.get()) { - return true; - } - - // Otherwise, check whether such request is happened - // after last successful request plus idle connection timeout. - long lastRequestTime = lastRequest.get(); - return (lastRequestTime + idleConnectionTimeout.toNanos() < System.nanoTime()); - } - private CompletionStage sendAsync11WithInitialRequestPermit( Message query, Executor executor, long startTime, Object requestBuilder, Permit initialRequestPermit) { - boolean isInitialRequest = checkInitialRequest(); + long lastRequestTime = lastRequest.get(); + boolean isInitialRequest = idleConnectionTimeout.toNanos() > getNanoTime() - lastRequestTime; if (!isInitialRequest) { initialRequestPermit.release(); } // check if we already exceeded the query timeout while checking the initial connection - Duration remainingTimeout = timeout.minus(System.nanoTime() - startTime, ChronoUnit.NANOS); + Duration remainingTimeout = timeout.minus(getNanoTime() - startTime, ChronoUnit.NANOS); if (remainingTimeout.isNegative()) { if (isInitialRequest) { initialRequestPermit.release(); @@ -532,25 +520,6 @@ private CompletionStage sendAsync11WithInitialRequestPermit( .thenCompose(Function.identity()); } - /** - * Set last request time to {@link DohResolver#lastRequest}, which ensures only the largest timestamp could be accepted. - * - * @param startTime start time in nanos of a Doh request. - */ - private void setLastRequestTime(long startTime) { - long current = lastRequest.get(); - // Only update value of 'lastRequest' if timestamp in 'lastRequest' is smaller than incoming 'startTime' value. - if (current < startTime) { - while (!lastRequest.compareAndSet(current, startTime)) { - // CAS failed, re-verify the eligibility of timestamp in 'lastRequest' to be updated to the incoming 'startTime' value. - current = lastRequest.get(); - if (current > startTime) { - return; - } - } - } - } - private CompletionStage sendAsync11WithConcurrentRequestPermit( Message query, Executor executor, @@ -560,7 +529,7 @@ private CompletionStage sendAsync11WithConcurrentRequestPermit( boolean isInitialRequest, Permit maxConcurrentRequestPermit) { // check if the stream lock acquisition took too long - Duration remainingTimeout = timeout.minus(System.nanoTime() - startTime, ChronoUnit.NANOS); + Duration remainingTimeout = timeout.minus(getNanoTime() - startTime, ChronoUnit.NANOS); if (remainingTimeout.isNegative()) { if (isInitialRequest) { initialRequestPermit.release(); @@ -583,12 +552,7 @@ private CompletionStage sendAsync11WithConcurrentRequestPermit( .whenComplete( (result, ex) -> { if (ex == null) { - setLastRequestTime(startTime); - if (isInitialRequest) { - // initial request was completed successfully, so toggle initialRequestSentMark to true. - // it's very safe to toggle initialRequestSentMark here, since this code had been guarded by initialRequestLock and its permit outside. - initialRequestSentMark.compareAndSet(false, true); - } + lastRequest.set(startTime); } maxConcurrentRequestPermit.release(); if (isInitialRequest) { diff --git a/src/test/java/org/xbill/DNS/DohResolverTest.java b/src/test/java/org/xbill/DNS/DohResolverTest.java index 98f489c1..26daf19c 100644 --- a/src/test/java/org/xbill/DNS/DohResolverTest.java +++ b/src/test/java/org/xbill/DNS/DohResolverTest.java @@ -1,6 +1,11 @@ // SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; + import io.netty.handler.codec.http.HttpHeaderNames; import io.vertx.core.Future; import io.vertx.core.Vertx; @@ -18,16 +23,19 @@ import java.util.Base64; import java.util.Collections; import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; - -import static org.junit.jupiter.api.Assertions.*; +import org.mockito.stubbing.Answer; @ExtendWith(VertxExtension.class) class DohResolverTest { @@ -153,88 +161,6 @@ void initialRequestSlowResolve(Vertx vertx, VertxTestContext context) { }); } - - @Test - void initialRequestGuardIfIdleConnectionTimeIsLargerThanSystemNanoTime(Vertx vertx, VertxTestContext context) { - if (isPreJava9()) { - System.out.println("Current JVM is PreJava9, no need to run such test."); - context.completeNow(); - return; - } - resolver = new DohResolver("http://localhost", - 2, - // so long idleConnectionTimeout - // in order to hack the condition for checking initial request in org.xbill.DNS.DohResolver.checkInitialRequest - Duration.ofNanos(System.nanoTime() + Duration.ofSeconds(100L).toNanos())); - resolver.setTimeout(Duration.ofSeconds(1)); - // Just add a 100ms delay before responding to the 1st call - // to simulate a 'concurrent doh request' for the 2nd call, - // then let the fake dns server respond to the 2nd call ASAP. - allRequestsUseTimeout = false; - - // idleConnectionTimeout = 2s, lastRequest = 0L - // Ensure lastRequest + idleConnectionTimeout < System.nanoTime() (3s) - - // Timeline: - // |<-------- 100ms -------->| - // ↑ ↑ - // 1st call sent response of 1st call - // |20ms|<------ 80ms ------>|<------ few millis ------->| - // ↑ wait until 1st call ↑ ↑ - // 2nd call begin 2nd call sent response of 2nd call - - AtomicBoolean firstCallCompleted = new AtomicBoolean(false); - - setupResolverWithServer(Duration.ofMillis(100L), - 200, - 2, - vertx, - context) - .onSuccess( - server -> { - // First call - CompletionStage firstCall = resolver.sendAsync(qm); - // Ensure second call was made after first call. - sleepNotThrown(20L); - CompletionStage secondCall = resolver.sendAsync(qm); - - Future.fromCompletionStage(firstCall) - .onComplete( - context.succeeding( - result -> - context.verify( - () -> { - assertEquals(Rcode.NOERROR, result.getHeader().getRcode()); - assertEquals(0, result.getHeader().getID()); - assertEquals(queryName, result.getQuestion().getName()); - firstCallCompleted.set(true); - }))); - - Future.fromCompletionStage(secondCall) - .onComplete( - context.succeeding( - result -> - context.verify( - () -> { - assertTrue(firstCallCompleted.get()); - assertEquals(Rcode.NOERROR, result.getHeader().getRcode()); - assertEquals(0, result.getHeader().getID()); - assertEquals(queryName, result.getQuestion().getName()); - // Complete context after the 2nd call was completed. - context.completeNow(); - }))); - } - ); - } - - private static void sleepNotThrown(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - @Test void initialRequestTimeoutResolve(Vertx vertx, VertxTestContext context) { resolver = new DohResolver("http://localhost", 2, Duration.ofMinutes(2)); @@ -275,10 +201,6 @@ void initialRequestTimeoutResolve(Vertx vertx, VertxTestContext context) { }); } - private static boolean isPreJava9() { - return System.getProperty("java.version").startsWith("1."); - } - private Future setupResolverWithServer( Duration responseDelay, int statusCode, @@ -289,6 +211,75 @@ private Future setupResolverWithServer( .onSuccess(server -> resolver.setUriTemplate("http://localhost:" + server.actualPort())); } + @EnabledForJreRange( + min = JRE.JAVA_9, + disabledReason = "Java 8 implementation doesn't have the initial request guard") + @Test + void initialRequestGuardIfIdleConnectionTimeIsLargerThanSystemNanoTime( + Vertx vertx, VertxTestContext context) { + AtomicLong startNanos = new AtomicLong(System.nanoTime()); + resolver = spy(new DohResolver("http://localhost", 2, Duration.ofMinutes(2))); + resolver.setTimeout(Duration.ofSeconds(1)); + // Simulate a nanoTime value that is lower than the idle timeout + doAnswer((Answer) invocationOnMock -> System.nanoTime() - startNanos.get()) + .when(resolver) + .getNanoTime(); + + // Just add a 100ms delay before responding to the 1st call + // to simulate a 'concurrent doh request' for the 2nd call, + // then let the fake dns server respond to the 2nd call ASAP. + allRequestsUseTimeout = false; + + // idleConnectionTimeout = 2s, lastRequest = 0L + // Ensure idleConnectionTimeout < System.nanoTime() - lastRequest (3s) + + // Timeline: + // |<-------- 100ms -------->| + // ↑ ↑ + // 1st call sent response of 1st call + // |20ms|<------ 80ms ------>|<------ few millis ------->| + // ↑ wait until 1st call ↑ ↑ + // 2nd call begin 2nd call sent response of 2nd call + + AtomicBoolean firstCallCompleted = new AtomicBoolean(false); + + setupResolverWithServer(Duration.ofMillis(100L), 200, 2, vertx, context) + .onSuccess( + server -> { + // First call + CompletionStage firstCall = resolver.sendAsync(qm); + // Ensure second call was made after first call and uses a different query + startNanos.addAndGet(TimeUnit.MILLISECONDS.toNanos(20)); + CompletionStage secondCall = resolver.sendAsync(Message.newQuery(qr)); + + Future.fromCompletionStage(firstCall) + .onComplete( + context.succeeding( + result -> + context.verify( + () -> { + assertEquals(Rcode.NOERROR, result.getHeader().getRcode()); + assertEquals(0, result.getHeader().getID()); + assertEquals(queryName, result.getQuestion().getName()); + firstCallCompleted.set(true); + }))); + + Future.fromCompletionStage(secondCall) + .onComplete( + context.succeeding( + result -> + context.verify( + () -> { + assertTrue(firstCallCompleted.get()); + assertEquals(Rcode.NOERROR, result.getHeader().getRcode()); + assertEquals(0, result.getHeader().getID()); + assertEquals(queryName, result.getQuestion().getName()); + // Complete context after the 2nd call was completed. + context.completeNow(); + }))); + }); + } + private Future setupServer( Message expectedDnsRequest, Message dnsResponse, @@ -298,7 +289,7 @@ private Future setupServer( VertxTestContext context, Vertx vertx) { HttpVersion version = - isPreJava9() + System.getProperty("java.version").startsWith("1.") ? HttpVersion.HTTP_1_1 : HttpVersion.HTTP_2; AtomicInteger requestCount = new AtomicInteger(0);