|
| 1 | +package com.blazemeter.jmeter.http2.core; |
| 2 | + |
| 3 | +import java.net.InetAddress; |
| 4 | +import java.net.InetSocketAddress; |
| 5 | +import java.net.UnknownHostException; |
| 6 | +import java.util.ArrayList; |
| 7 | +import java.util.List; |
| 8 | +import java.util.Map; |
| 9 | +import java.util.concurrent.Executor; |
| 10 | +import java.util.concurrent.TimeUnit; |
| 11 | +import java.util.concurrent.TimeoutException; |
| 12 | +import java.util.concurrent.atomic.AtomicBoolean; |
| 13 | +import java.util.function.Supplier; |
| 14 | +import org.apache.http.conn.DnsResolver; |
| 15 | +import org.eclipse.jetty.util.Promise; |
| 16 | +import org.eclipse.jetty.util.SocketAddressResolver; |
| 17 | +import org.eclipse.jetty.util.thread.Scheduler; |
| 18 | + |
| 19 | +/** |
| 20 | + * Resolves host names through JMeter's DNS Cache Manager instead of {@code InetAddress}, so a test |
| 21 | + * plan's custom DNS servers, static host entries and per-thread DNS cache apply to this plugin |
| 22 | + * exactly as they do to {@code HTTPHC4Impl}. |
| 23 | + * |
| 24 | + * <p>{@code DNSCacheManager} is an {@code org.apache.http.conn.DnsResolver}, which is what |
| 25 | + * {@code HTTPHC4Impl} hands to its connection operator on the one-time client init. This class is |
| 26 | + * the Jetty-side equivalent: {@code HttpClient} resolves through a {@link SocketAddressResolver}, |
| 27 | + * and installing one is enough to cover every protocol, because HTTP/1.1, h2, h2c and HTTP/3 all |
| 28 | + * reach the network through {@code HttpClient.newConnection}. |
| 29 | + * |
| 30 | + * <p>Deliberately modelled on {@link SocketAddressResolver.Async}, which it replaces: the lookup |
| 31 | + * runs on the client executor rather than on the caller (which may be a selector thread), and a |
| 32 | + * scheduled task fails the promise if the lookup outlives the timeout. The guard is not optional |
| 33 | + * here - JMeter never calls {@code DNSCacheManager.setTimeoutMs}, so a custom resolver inherits |
| 34 | + * dnsjava's own retry behaviour and a black-holed DNS server would otherwise pin an executor |
| 35 | + * thread with nothing failing the request. |
| 36 | + * |
| 37 | + * <p>The executor and scheduler are read lazily because the resolver is installed while the |
| 38 | + * {@code HttpClient} is still being built: {@code HttpClient.doStart} only creates its default |
| 39 | + * {@code Async} resolver when none was set, so ours has to be in place before {@code start()}, |
| 40 | + * at which point {@code getExecutor()} and {@code getScheduler()} are still {@code null}. |
| 41 | + */ |
| 42 | +public class JMeterDnsSocketAddressResolver implements SocketAddressResolver { |
| 43 | + |
| 44 | + private final DnsResolver dnsResolver; |
| 45 | + private final Supplier<Executor> executorSupplier; |
| 46 | + private final Supplier<Scheduler> schedulerSupplier; |
| 47 | + private final long timeoutMs; |
| 48 | + |
| 49 | + public JMeterDnsSocketAddressResolver(DnsResolver dnsResolver, |
| 50 | + Supplier<Executor> executorSupplier, |
| 51 | + Supplier<Scheduler> schedulerSupplier, |
| 52 | + long timeoutMs) { |
| 53 | + this.dnsResolver = dnsResolver; |
| 54 | + this.executorSupplier = executorSupplier; |
| 55 | + this.schedulerSupplier = schedulerSupplier; |
| 56 | + this.timeoutMs = timeoutMs; |
| 57 | + } |
| 58 | + |
| 59 | + @Override |
| 60 | + public void resolve(String host, int port, Map<String, Object> context, |
| 61 | + Promise<List<InetSocketAddress>> promise) { |
| 62 | + Executor executor = executorSupplier.get(); |
| 63 | + if (executor == null) { |
| 64 | + // Only reachable if a caller resolves before the client is started; resolving inline is |
| 65 | + // still better than dropping the request on the floor. |
| 66 | + resolveAndComplete(host, port, promise, new AtomicBoolean()); |
| 67 | + return; |
| 68 | + } |
| 69 | + executor.execute(() -> { |
| 70 | + AtomicBoolean complete = new AtomicBoolean(); |
| 71 | + Scheduler.Task timeoutTask = scheduleTimeout(host, Thread.currentThread(), complete, promise); |
| 72 | + try { |
| 73 | + resolveAndComplete(host, port, promise, complete); |
| 74 | + } finally { |
| 75 | + if (timeoutTask != null) { |
| 76 | + timeoutTask.cancel(); |
| 77 | + } |
| 78 | + // The timeout task interrupts this thread to unblock the lookup; clear the flag so the |
| 79 | + // pooled thread does not carry it into unrelated work. |
| 80 | + Thread.interrupted(); |
| 81 | + } |
| 82 | + }); |
| 83 | + } |
| 84 | + |
| 85 | + private Scheduler.Task scheduleTimeout(String host, Thread resolvingThread, |
| 86 | + AtomicBoolean complete, |
| 87 | + Promise<List<InetSocketAddress>> promise) { |
| 88 | + Scheduler scheduler = schedulerSupplier.get(); |
| 89 | + if (timeoutMs <= 0 || scheduler == null) { |
| 90 | + return null; |
| 91 | + } |
| 92 | + return scheduler.schedule(() -> { |
| 93 | + if (complete.compareAndSet(false, true)) { |
| 94 | + promise.failed(new TimeoutException( |
| 95 | + "DNS timeout " + timeoutMs + " ms resolving " + host)); |
| 96 | + resolvingThread.interrupt(); |
| 97 | + } |
| 98 | + }, timeoutMs, TimeUnit.MILLISECONDS); |
| 99 | + } |
| 100 | + |
| 101 | + private void resolveAndComplete(String host, int port, |
| 102 | + Promise<List<InetSocketAddress>> promise, |
| 103 | + AtomicBoolean complete) { |
| 104 | + try { |
| 105 | + InetAddress[] addresses = resolveAddresses(host); |
| 106 | + // DNSCacheManager returns null when a custom lookup cannot parse the name, and an empty |
| 107 | + // array when a static host entry is matched case-insensitively by isStaticHost but read |
| 108 | + // case-sensitively by fromStaticHost (a JMeter bug still present on master). Neither may |
| 109 | + // reach Jetty as a success: HttpClient indexes straight into the returned list. |
| 110 | + if (addresses == null || addresses.length == 0) { |
| 111 | + throw new UnknownHostException(host); |
| 112 | + } |
| 113 | + List<InetSocketAddress> result = new ArrayList<>(addresses.length); |
| 114 | + for (InetAddress address : addresses) { |
| 115 | + result.add(new InetSocketAddress(address, port)); |
| 116 | + } |
| 117 | + if (complete.compareAndSet(false, true)) { |
| 118 | + promise.succeeded(result); |
| 119 | + } |
| 120 | + } catch (Throwable failure) { |
| 121 | + if (complete.compareAndSet(false, true)) { |
| 122 | + promise.failed(failure); |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + private InetAddress[] resolveAddresses(String host) throws UnknownHostException { |
| 128 | + // DNSCacheManager keeps its cache in a plain LinkedHashMap and JMeter clones one instance per |
| 129 | + // thread, but a single JMeter thread resolves concurrently here (embedded resources, and the |
| 130 | + // HTTP/3 vs HTTP/2 race, run on plugin executors). Serializing keeps that map consistent; |
| 131 | + // cache hits make the critical section negligible. |
| 132 | + synchronized (dnsResolver) { |
| 133 | + return dnsResolver.resolve(host); |
| 134 | + } |
| 135 | + } |
| 136 | +} |
0 commit comments